﻿using System;
using System.IO;
using System.Linq;
using System.Windows.Input;
using FluentValidation;
using LINQPad.Extensibility.DataContext;
using LINQPad.OpenAccess.Helpers;
using LINQPad.OpenAccess.Metadata;
using LINQPad.OpenAccess.Services;
using LINQPad.OpenAccess.Settings;
using LINQPad.OpenAccess.UI.Commands;
using LINQPad.OpenAccess.UI.Services;
using Telerik.OpenAccess.Metadata;

namespace LINQPad.OpenAccess.UI.ViewModel
{
    /// <summary>
    /// Basic settings view model.
    /// </summary>
    public class BasicSettingsViewModel : DriverSubSettingsViewModelBase<BasicSettingsViewModel>, IPersistableSettingsViewModel<DriverSettings>
    {
        #region Private

        #region - Vars

        private DelegateCommand<object> __chooseAssemblyCommand, __refreshCommand, __chooseAppConfigCommand;
        private IUserInteractionService _userInteractionService;
        private IBackendService _backendService;
        private IContextTypeInfo _selectedContextType;
        private IContextTypeInfo[] _contextTypes;
        private IMetadataSourceInfoWithSource _selectedMetadataSource;
        private IMetadataSourceInfoWithSource[] _metadataSources;
        private bool _useDefaultMetadataSource, _useDefaultConstructor;
        private IAssemblyParser _assemblyParser;
        private string _appConfigFilePath, _assemblyFilePath;

        #endregion

        #region - Command Handlers

        /// <summary>
        /// Handles the 'CanExecute' event of the choose app config command.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        private bool ChooseAppConfig_CanExecute( object parameter )
        {
            return true;
        }
        /// <summary>
        /// Handles the 'Execute' event of the choose app config command.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        private void ChooseAppConfig_Execute( object parameter )
        {
            string appConfigFilePath;
            if ( this._userInteractionService.TryChooseAppConfig( out appConfigFilePath ) )
                this.AppConfigFilePath = appConfigFilePath;
        }
        /// <summary>
        /// Handles the 'CanExecute' event of the choose assembly command.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        private bool ChooseAssembly_CanExecute( object parameter )
        {
            return true;
        }
        /// <summary>
        /// Handles the 'Execute' event of the choose assembly command.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        private void ChooseAssembly_Execute( object parameter )
        {
            string assemblyFilePath;
            if ( this._userInteractionService.TryChooseAssembly( out assemblyFilePath ) )
                this.AssemblyFilePath = assemblyFilePath;
        }
        /// <summary>
        /// Handles the 'CanExecute' event of the refresh command.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        private bool Refresh_CanExecute( object parameter )
        {
            return !String.IsNullOrWhiteSpace( this.AssemblyFilePath );
        }
        /// <summary>
        /// Handles the 'Execute' event of the refresh command.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        private void Refresh_Execute( object parameter )
        {
            this.Refresh( );
        }

        #endregion

        #region - Functions

        /// <summary>
        /// Selects the context type by its name.
        /// </summary>
        /// <param name="contextTypeName">Name of the context type.</param>
        private void SelectContextTypeByName( string contextTypeName )
        {
            var contextTypes = this._contextTypes;
            if ( String.IsNullOrWhiteSpace( contextTypeName )
                    || contextTypes == null
                    || contextTypes.Length == 0 )
                this.SelectedContextType = null;
            else
            {
                // strip off assembly name
                contextTypeName = contextTypeName.Split( ',' )[0];
                this.SelectedContextType = contextTypes.FirstOrDefault
                                                                    (
                                                                        e => e.ContextType != null
                                                                                && contextTypeName.Equals( e.ContextType.FullName )
                                                                    );
            }
        }
        /// <summary>
        /// Selects the metadata source by the specified info.
        /// </summary>
        /// <param name="metadataSourceInfo">The metadata source info.</param>
        private void SelectMetadataSourceByInfo( IMetadataSourceInfo metadataSourceInfo )
        {
            if ( metadataSourceInfo == null )
            {
                this.SelectedMetadataSource = null;
                this.UseDefaultMetadataSource = this.CanUseDefaultMetadataSource;
            }
            else
            {
                var cmp = MetadataSourceInfoEqualityComparer.Default;
                this.SelectedMetadataSource = this._metadataSources.FirstOrDefault( t => cmp.Equals( t, metadataSourceInfo ) );
                this.UseDefaultMetadataSource = this.SelectedMetadataSource == null && this.CanUseDefaultMetadataSource;
            }
        }
        /// <summary>
        /// Syncs the default backend with the specified owner.
        /// </summary>
        /// <param name="owner">The owner.</param>
        private void SyncDefaultBackendWithOwner( DriverSettingsViewModel owner )
        {
            BackendSettingsViewModel backendSettings;
            if ( owner == null || null == ( backendSettings = owner.BackendSettings ) )
                return;

            var defaultBackendConfiguration = this._selectedContextType == null || this._selectedContextType.DefaultBackendConfiguration == null
                                                    ? null
                                                    : this._selectedContextType.DefaultBackendConfiguration;

            Backend defaultBackend;
            if ( defaultBackendConfiguration == null )
                backendSettings.SetDefaultBackend( null, null );
            else if ( !this._backendService.TryGetBackendFromConfigurationName( defaultBackendConfiguration.Backend, out defaultBackend ) )
                backendSettings.SetDefaultBackend( null, defaultBackendConfiguration.ProviderName );
            else
                backendSettings.SetDefaultBackend( defaultBackend, defaultBackendConfiguration.ProviderName );
        }

        #endregion

        #endregion

        #region Protected

        #region - Functions

        /// <summary>
        /// Raises the 'CanExecuteChanged' event for the view model's commands.
        /// </summary>
        protected virtual void RaiseCanExecuteChangedForCommands( )
        {
            if ( this.__chooseAssemblyCommand != null )
                this.__chooseAssemblyCommand.RaiseCanExecuteChanged( );
            if ( this.__chooseAppConfigCommand != null )
                this.__chooseAppConfigCommand.RaiseCanExecuteChanged( );
            if ( this.__refreshCommand != null )
                this.__refreshCommand.RaiseCanExecuteChanged( );
        }
        /// <summary>
        /// Parses the assembly file.
        /// </summary>
        /// <returns>
        /// The assembly parse result.
        /// </returns>
        protected virtual IAssemblyParseResult ParseAssemblyFile( )
        {
            if ( !AssemblyFilePathExists )
                return AssemblyParseResult.Empty;
            return this._assemblyParser.Parse( this.AssemblyFilePath );
        }

        #endregion

        #endregion

        #region Public

        #region - Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="DriverSettingsViewModel" /> class.
        /// </summary>
        /// <param name="assemblyParser">The assembly parser.</param>
        /// <param name="appConfigService">The app config service.</param>
        /// <param name="userInteractionService">The user interaction service.</param>
        /// <param name="validator">The validator.</param>
        public BasicSettingsViewModel( IAssemblyParser assemblyParser, IBackendService backendService, IUserInteractionService userInteractionService
                                        , IValidator<BasicSettingsViewModel> validator )
            : base( validator )
        {
            Guard.ArgumentNotNull( assemblyParser, "assemblyParser" );
            Guard.ArgumentNotNull( userInteractionService, "userInteractionService" );
            Guard.ArgumentNotNull( validator, "validator" );

            this._assemblyParser = assemblyParser;
            this._backendService = backendService;
            this._userInteractionService = userInteractionService;
        }

        #endregion

        #region - Functions

        /// <summary>
        /// Refreshes this instance.
        /// </summary>
        public void Refresh( )
        {
            if ( !this.AssemblyFilePathExists )
            {
                this.ContextTypes = null;
                this.MetadataSources = null;
                this.SelectedMetadataSource = null;
                this.SelectedContextType = null;
            }
            else
            {
                var result = this.ParseAssemblyFile( );
                this.ContextTypes = result.ContextTypes;
                this.MetadataSources = result.MetadataSources;
            }
        }

        #endregion

        #region - Properties

        /// <summary>
        /// Gets or sets the app config file path.
        /// </summary>
        /// <value>
        /// The app config file path.
        /// </value>
        public string AppConfigFilePath
        {
            get
            {
                return this._appConfigFilePath;
            }
            set
            {
                if ( this._appConfigFilePath != value )
                {
                    this._appConfigFilePath = value;
                    this.OnPropertyChanged( ( ) => this.AppConfigFilePath );
                    this.OnPropertyChanged( ( ) => this.AppConfigFilePathExists );

                    this.Invalidate( );
                }
            }
        }
        /// <summary>
        /// Gets a value indicating whether the assembly file path exists.
        /// </summary>
        /// <value>
        /// <c>true</c> if the assembly file path exists; otherwise, <c>false</c>.
        /// </value>
        public bool AppConfigFilePathExists
        {
            get
            {
                var filePath = this.AppConfigFilePath;
                return !String.IsNullOrWhiteSpace( filePath )
                        && File.Exists( filePath );
            }
        }
        /// <summary>
        /// Gets or sets the assembly file path.
        /// </summary>
        /// <value>
        /// The assembly file path.
        /// </value>
        public string AssemblyFilePath
        {
            get
            {
                return this._assemblyFilePath;
            }
            set
            {
                if ( this._assemblyFilePath != value )
                {
                    this._assemblyFilePath = value;
                    this.OnPropertyChanged( ( ) => this.AssemblyFilePath );
                    this.OnPropertyChanged( ( ) => this.AssemblyFilePathExists );

                    this.Refresh( );
                    this.RaiseCanExecuteChangedForCommands( );
                }
            }
        }
        /// <summary>
        /// Gets a value indicating whether the assembly file path exists.
        /// </summary>
        /// <value>
        /// <c>true</c> if the assembly file path exists; otherwise, <c>false</c>.
        /// </value>
        public bool AssemblyFilePathExists
        {
            get
            {
                var filePath = this.AssemblyFilePath;
                return !String.IsNullOrWhiteSpace( filePath )
                        && File.Exists( filePath );
            }
        }
        /// <summary>
        /// Gets a value indicating whether the default constructor can be used.
        /// </summary>
        /// <value>
        /// <c>true</c> if the default constructor can be used; otherwise, <c>false</c>.
        /// </value>
        public bool CanUseDefaultConstructor
        {
            get
            {
                return this._selectedContextType != null && this._selectedContextType.HasDefaultConstructor;
            }
        }
        /// <summary>
        /// Gets a value indicating whether the driver is currently able to use the default metadata source.
        /// </summary>
        /// <value>
        /// <c>true</c> if the driver is able to use the default metadata source; otherwise, <c>false</c>.
        /// </value>
        public bool CanUseDefaultMetadataSource
        {
            get
            {
                return this._selectedContextType != null
                    && this._selectedContextType.DefaultMetadataSource != null;
            }
        }
        /// <summary>
        /// Gets the context types.
        /// </summary>
        /// <value>
        /// The context types.
        /// </value>
        public IContextTypeInfo[] ContextTypes
        {
            get
            {
                return this._contextTypes;
            }
            private set
            {
                if ( this._contextTypes != value )
                {
                    this._contextTypes = value ?? new IContextTypeInfo[0];

                    this.OnPropertyChanged( ( ) => this.ContextTypes );
                    this.OnPropertyChanged( ( ) => this.HasContextTypes );

                    var selectedContextType = this._selectedContextType;
                    if ( selectedContextType == null )
                        selectedContextType = this._contextTypes.Length == 0 ? null : this._contextTypes[0];
                    else
                    {
                        var cmp = ContextTypeInfoEqualityComparer.Default;
                        selectedContextType = this._contextTypes.FirstOrDefault( t => cmp.Equals( t, selectedContextType ) );
                    }

                    this.SelectedContextType = selectedContextType;
                    this.RaiseCanExecuteChangedForCommands( );
                }
            }
        }
        /// <summary>
        /// Gets a value indicating whether this instance has context types.
        /// </summary>
        /// <value>
        /// <c>true</c> if this instance has context types; otherwise, <c>false</c>.
        /// </value>
        public bool HasContextTypes
        {
            get
            {
                return this._contextTypes != null && this._contextTypes.Length > 0;
            }
        }
        /// <summary>
        /// Gets a value indicating whether this instance has metadata sources.
        /// </summary>
        /// <value>
        /// <c>true</c> if this instance has metadata sources; otherwise, <c>false</c>.
        /// </value>
        public bool HasMetadataSources
        {
            get
            {
                return this._metadataSources != null && this._metadataSources.Length > 0;
            }
        }
        /// <summary>
        /// Gets the metadata sources.
        /// </summary>
        /// <value>
        /// The metadata sources.
        /// </value>
        public IMetadataSourceInfoWithSource[] MetadataSources
        {
            get
            {
                return this._metadataSources;
            }
            private set
            {
                if ( this._metadataSources != value )
                {
                    this._metadataSources = value ?? new IMetadataSourceInfoWithSource[0];

                    this.OnPropertyChanged( ( ) => this.MetadataSources );
                    this.OnPropertyChanged( ( ) => this.HasMetadataSources );

                    var selectedMetadataSource = this._selectedMetadataSource;
                    if ( selectedMetadataSource != null )
                        this.SelectMetadataSourceByInfo( selectedMetadataSource );

                    this.RaiseCanExecuteChangedForCommands( );
                }
            }
        }
        /// <summary>
        /// Selects the context type for the sepcified metadata source.
        /// </summary>
        /// <param name="contextType">Type of the context.</param>
        protected virtual void SelectMetadataSourceForContextType( IContextTypeInfo contextType )
        {
            this.SelectedMetadataSource = null;
            this.UseDefaultMetadataSource = contextType != null && contextType.DefaultMetadataSource != null;
        }
        /// <summary>
        /// Gets or sets the selected context type.
        /// </summary>
        /// <value>
        /// The selected context type.
        /// </value>
        public IContextTypeInfo SelectedContextType
        {
            get
            {
                return this._selectedContextType;
            }
            set
            {
                if ( this._selectedContextType != value )
                {
                    this._selectedContextType = value;
                    this.OnPropertyChanged( ( ) => this.SelectedContextType );

                    this.SelectMetadataSourceForContextType( value );

                    this.OnPropertyChanged( ( ) => this.CanUseDefaultMetadataSource );
                    this.OnPropertyChanged( ( ) => this.UseDefaultMetadataSource );
                    this.OnPropertyChanged( ( ) => this.CanUseDefaultConstructor );
                    this.OnPropertyChanged( ( ) => this.UseDefaultConstructor );

                    this.SyncDefaultBackendWithOwner( this.Owner );

                    this.Invalidate( );
                    this.RaiseCanExecuteChangedForCommands( );
                }
            }
        }
        /// <summary>
        /// Gets the selected metadata source.
        /// </summary>
        /// <value>
        /// The selected metadata source.
        /// </value>
        public IMetadataSourceInfoWithSource SelectedMetadataSource
        {
            get
            {
                if ( this.UseDefaultMetadataSource )
                    return null;

                return this._selectedMetadataSource;
            }
            set
            {
                if ( this._selectedMetadataSource != value )
                {
                    this._selectedMetadataSource = value;
                    this.OnPropertyChanged( ( ) => this.SelectedMetadataSource );

                    this.RaiseCanExecuteChangedForCommands( );
                    this.Invalidate( );
                }
            }
        }
        /// <summary>
        /// Gets a value indicating whether the default constructor should be used.
        /// </summary>
        /// <value>
        /// <c>true</c> if the default constructor should be used; otherwise, <c>false</c>.
        /// </value>
        public bool UseDefaultConstructor
        {
            get
            {
                return this.CanUseDefaultConstructor && this._useDefaultConstructor;
            }
            set
            {
                if ( this._useDefaultConstructor != value )
                {
                    this._useDefaultConstructor = value;
                    this.OnPropertyChanged( ( ) => this.UseDefaultConstructor );
                    this.OnPropertyChanged( ( ) => this.AppConfigFilePath ); // not so nice, but needed for validation refresh
                    this.RaiseCanExecuteChangedForCommands( );
                    this.Invalidate( );
                }
            }
        }
        /// <summary>
        /// Gets or sets a value indicating whether the default metadata source should be used.
        /// </summary>
        /// <value>
        /// <c>true</c> if the default metadata source should be used; otherwise, <c>false</c>.
        /// </value>
        public bool UseDefaultMetadataSource
        {
            get
            {
                if ( !this.CanUseDefaultMetadataSource )
                    return false;

                return this._useDefaultMetadataSource;
            }
            set
            {
                if ( this._useDefaultMetadataSource != value )
                {
                    this._useDefaultMetadataSource = value;
                    this.OnPropertyChanged( ( ) => this.UseDefaultMetadataSource );
                    this.OnPropertyChanged( ( ) => this.SelectedMetadataSource );

                    this.RaiseCanExecuteChangedForCommands( );
                    this.Invalidate( );
                }
            }
        }

        #endregion

        #region - Commands

        /// <summary>
        /// Gets the choose app config command.
        /// </summary>
        /// <value>
        /// The choose app config command.
        /// </value>
        public ICommand ChooseAppConfigCommand
        {
            get
            {
                this.ThrowIfDisposed( );
                return this.__chooseAppConfigCommand ?? ( this.__chooseAppConfigCommand = new DelegateCommand<object>( ChooseAppConfig_Execute, ChooseAppConfig_CanExecute ) );
            }
        }
        /// <summary>
        /// Gets the choose assembly command.
        /// </summary>
        /// <value>
        /// The choose assembly command.
        /// </value>
        public ICommand ChooseAssemblyCommand
        {
            get
            {
                this.ThrowIfDisposed( );
                return this.__chooseAssemblyCommand ?? ( this.__chooseAssemblyCommand = new DelegateCommand<object>( ChooseAssembly_Execute, ChooseAssembly_CanExecute ) );
            }
        }
        /// <summary>
        /// Gets the refresh command.
        /// </summary>
        /// <value>
        /// The refresh command.
        /// </value>
        public ICommand RefreshCommand
        {
            get
            {
                this.ThrowIfDisposed( );
                return this.__refreshCommand ?? ( this.__refreshCommand = new DelegateCommand<object>( Refresh_Execute, Refresh_CanExecute ) );
            }
        }

        #endregion

        #endregion

        #region DriverSubSettingsViewModelBase<BasicSettingsViewModel> Members

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected override void Dispose( bool disposing )
        {
            if ( disposing )
            {
                this.DisposeField( ref this.__chooseAssemblyCommand );
                this.DisposeField( ref this.__chooseAppConfigCommand );
                this.DisposeField( ref this.__refreshCommand );
                this._backendService = null;
                this._userInteractionService = null;
                this._assemblyParser = null;
            }
            base.Dispose( disposing );
        }
        /// <summary>
        /// Called when the owner has changed.
        /// </summary>
        /// <param name="newOwner">The new owner.</param>
        /// <param name="oldOwner">The old owner.</param>
        protected override void OnOwnerChanged( DriverSettingsViewModel newOwner, DriverSettingsViewModel oldOwner )
        {
            base.OnOwnerChanged( newOwner, oldOwner );
            this.SyncDefaultBackendWithOwner( newOwner );
        }

        #endregion

        #region IPersistableSettingsViewModel<DriverSettings> Members

        /// <summary>
        /// Tries to load the view model's settings from the specified sources.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <param name="source">The source.</param>
        /// <returns>
        ///   <c>true</c> on success; otherwise, <c>false</c>.
        /// </returns>
        public bool TryLoadSettingsFrom( IConnectionInfo connectionInfo, DriverSettings source )
        {
            if ( connectionInfo == null || source == null )
                return false;

            this.AssemblyFilePath = connectionInfo.CustomTypeInfo.CustomAssemblyPath;
            this.SelectContextTypeByName( connectionInfo.CustomTypeInfo.CustomTypeName );
            this.SelectMetadataSourceByInfo( source.MetadataSource );
            this.AppConfigFilePath = connectionInfo.AppConfigPath;
            this.UseDefaultConstructor = source.Initialization != null && source.Initialization.UseDefaultConstructor;

            return true;
        }
        /// <summary>
        /// Writes the view model's settings to the specified targets.
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <param name="target">The target.</param>
        public void WriteSettingsTo( IConnectionInfo connectionInfo, DriverSettings target )
        {
            Guard.ArgumentNotNull( connectionInfo, "connectionInfo" );
            Guard.ArgumentNotNull( target, "target" );

            connectionInfo.CustomTypeInfo.CustomAssemblyPath = this.AssemblyFilePath;

            Type contextType;

            if ( this._selectedContextType == null || null == ( contextType = this._selectedContextType.ContextType ) )
            {
                connectionInfo.CustomTypeInfo.CustomTypeName = null;
                target.AssemblyQualifiedContextTypeName = null;
            }
            else
            {
                connectionInfo.CustomTypeInfo.CustomTypeName = contextType.FullName;
                target.AssemblyQualifiedContextTypeName = String.Format( "{0}, {1}", contextType.FullName, contextType.Assembly.GetName( ).Name );
            }

            target.MetadataSource = this.UseDefaultMetadataSource || this._selectedMetadataSource == null
                                        ? null
                                        : new MetadataSourceSettings( this._selectedMetadataSource );
            connectionInfo.AppConfigPath = this.AppConfigFilePath;

            if ( target.Initialization == null )
                target.Initialization = new InitializationSettings( );
            target.Initialization.UseDefaultConstructor = this.UseDefaultConstructor;
        }

        #endregion
    }
}
